/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.maven.plugin.surefire.report;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;

import org.apache.maven.surefire.api.report.ReportEntry;
import org.apache.maven.surefire.shared.utils.logging.MessageBuilder;

import static org.apache.maven.surefire.api.report.CategorizedReportEntry.GROUP_PREFIX;
import static org.apache.maven.surefire.shared.utils.logging.MessageUtils.buffer;

/**
 * Maintains per-thread test result state. Not thread safe.
 */
public class TestSetStats {
    private static final String TESTS = "Tests ";
    private static final String RUN = "run: ";
    private static final String TESTS_RUN = "Tests run: ";
    private static final String FAILURES = "Failures: ";
    private static final String ERRORS = "Errors: ";
    private static final String SKIPPED = "Skipped: ";
    private static final String FAILURE_MARKER = " <<< FAILURE!";
    private static final String IN_MARKER = " -- in ";
    private static final String COMMA = ", ";

    private final Queue<WrappedReportEntry> reportEntries = new ConcurrentLinkedQueue<>();

    private final boolean trimStackTrace;

    private final boolean plainFormat;

    private long testSetStartAt;

    private long testStartAt;

    private int completedCount;

    private int errors;

    private int failures;

    private int skipped;

    private long lastStartAt;

    public TestSetStats(boolean trimStackTrace, boolean plainFormat) {
        this.trimStackTrace = trimStackTrace;
        this.plainFormat = plainFormat;
    }

    public int getElapsedSinceTestSetStart() {
        return testSetStartAt > 0 ? (int) (System.currentTimeMillis() - testSetStartAt) : 0;
    }

    public int getElapsedSinceLastStart() {
        return lastStartAt > 0 ? (int) (System.currentTimeMillis() - lastStartAt) : 0;
    }

    public void testSetStart() {
        testSetStartAt = System.currentTimeMillis();
        lastStartAt = testSetStartAt;
    }

    public void testStart() {
        testStartAt = System.currentTimeMillis();
        lastStartAt = testStartAt;
    }

    private void finishTest(WrappedReportEntry reportEntry) {
        reportEntries.add(reportEntry);
        incrementCompletedCount();
        // SUREFIRE-398 skipped tests call endTest without calling testStarting
        // if startTime = 0, set it to endTime, so the diff will be 0
        if (testStartAt == 0) {
            testStartAt = System.currentTimeMillis();
        }
    }

    public void testSucceeded(WrappedReportEntry reportEntry) {
        finishTest(reportEntry);
    }

    public void testError(WrappedReportEntry reportEntry) {
        errors += 1;
        finishTest(reportEntry);
    }

    public void testFailure(WrappedReportEntry reportEntry) {
        failures += 1;
        finishTest(reportEntry);
    }

    public void testSkipped(WrappedReportEntry reportEntry) {
        skipped += 1;
        finishTest(reportEntry);
    }

    public void reset() {
        completedCount = 0;
        errors = 0;
        failures = 0;
        skipped = 0;

        for (WrappedReportEntry entry : reportEntries) {
            entry.getStdout().free();
            entry.getStdErr().free();
        }

        reportEntries.clear();
    }

    public int getCompletedCount() {
        return completedCount;
    }

    public int getErrors() {
        return errors;
    }

    public int getFailures() {
        return failures;
    }

    public int getSkipped() {
        return skipped;
    }

    private void incrementCompletedCount() {
        completedCount += 1;
    }

    public String getTestSetSummary(WrappedReportEntry reportEntry, boolean phrasedClassName) {
        String summary = TESTS_RUN
                + completedCount
                + COMMA
                + FAILURES
                + failures
                + COMMA
                + ERRORS
                + errors
                + COMMA
                + SKIPPED
                + skipped
                + COMMA
                + reportEntry.getElapsedTimeVerbose();

        if (failures > 0 || errors > 0) {
            summary += FAILURE_MARKER;
        }

        summary += IN_MARKER;
        summary += phrasedClassName ? reportEntry.getReportNameWithGroup() : reportEntry.getNameWithGroup();

        return summary;
    }

    public String getColoredTestSetSummary(WrappedReportEntry reportEntry, boolean phrasedClassName) {
        final boolean isSuccessful = failures == 0 && errors == 0 && skipped == 0;
        final boolean isFailure = failures > 0;
        final boolean isError = errors > 0;
        final boolean isFailureOrError = isFailure | isError;
        final boolean isSkipped = skipped > 0;
        final MessageBuilder builder = buffer();
        if (isSuccessful) {
            if (completedCount == 0) {
                builder.strong(TESTS_RUN).strong(completedCount);
            } else {
                builder.success(TESTS_RUN).success(completedCount);
            }
        } else {
            if (isFailureOrError) {
                builder.failure(TESTS).strong(RUN).strong(completedCount);
            } else {
                builder.warning(TESTS).strong(RUN).strong(completedCount);
            }
        }
        builder.a(COMMA);
        if (isFailure) {
            builder.failure(FAILURES).failure(failures);
        } else {
            builder.a(FAILURES).a(failures);
        }
        builder.a(COMMA);
        if (isError) {
            builder.failure(ERRORS).failure(errors);
        } else {
            builder.a(ERRORS).a(errors);
        }
        builder.a(COMMA);
        if (isSkipped) {
            builder.warning(SKIPPED).warning(skipped);
        } else {
            builder.a(SKIPPED).a(skipped);
        }
        builder.a(COMMA).a(reportEntry.getElapsedTimeVerbose());
        if (isFailureOrError) {
            builder.failure(FAILURE_MARKER);
        }
        builder.a(IN_MARKER);
        return concatenateWithTestGroup(builder, reportEntry, phrasedClassName);
    }

    public List<String> getTestResults() {
        List<String> result = new ArrayList<>();
        for (WrappedReportEntry testResult : reportEntries) {
            if (testResult.isErrorOrFailure()) {
                result.add(testResult.getOutput(trimStackTrace));
            } else if (plainFormat && testResult.isSkipped()) {
                result.add(testResult.getSourceName() + " skipped");
            } else if (plainFormat && testResult.isSucceeded()) {
                result.add(testResult.getElapsedTimeSummary());
            }
        }
        // This should be Map with an enum and the enums will be displayed with colors on console.
        return result;
    }

    public Collection<WrappedReportEntry> getReportEntries() {
        return reportEntries;
    }

    /**
     * Append the test set message for a report.
     * e.g. "org.foo.BarTest ( of group )" or phrased text "test class description ( of group )".
     *
     * @param builder    MessageBuilder with preceded text inside
     * @param report     report whose test set is starting
     * @return the message
     */
    static String concatenateWithTestGroup(MessageBuilder builder, ReportEntry report, boolean phrasedClassName) {
        if (phrasedClassName && report.getReportNameWithGroup() != null) {
            return builder.strong(report.getReportNameWithGroup()).toString();
        } else {
            String testClass = report.getNameWithGroup();
            int indexOfGroup = testClass.indexOf(GROUP_PREFIX);
            int delimiter = testClass.lastIndexOf('.', indexOfGroup == -1 ? testClass.length() : indexOfGroup);
            String pkg = testClass.substring(0, 1 + delimiter);
            String cls = testClass.substring(1 + delimiter);
            return builder.a(pkg).strong(cls).toString();
        }
    }
}
